package com.qdlc.p2p.biz.service.impl;

import java.io.Serializable;
import java.net.URLDecoder;
import java.text.MessageFormat;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;

import com.qdlc.p2p.biz.log.score.login.LoginScoreLog;
import com.qdlc.p2p.biz.service.UserCacheService;
import com.qdlc.p2p.biz.service.UserLoginExtService;
import com.qdlc.p2p.biz.service.UserLoginService;
import com.qdlc.p2p.biz.util.UserLoginUtil;
import com.qdlc.p2p.biz.util.UserReturnResult;
import com.qdlc.p2p.common.constant.Global;
import com.qdlc.p2p.common.core.ReturnResult;
import com.qdlc.p2p.common.core.redis.JedisCache;
import com.qdlc.p2p.common.core.redis.JedisCacheFactory;
import com.qdlc.p2p.common.util.DateUtil;
import com.qdlc.p2p.common.util.MessageUtil;
import com.qdlc.p2p.common.util.StringUtil;
import com.qdlc.p2p.common.util.ValidateUtil;
import com.qdlc.p2p.common.util.code.MD5;
import com.qdlc.p2p.dal.checkrule.LoginRuleCheck;
import com.qdlc.p2p.dal.common.RuleCheckHelper;
import com.qdlc.p2p.dal.dao.ScoreDao;
import com.qdlc.p2p.dal.dto.Score;
import com.qdlc.p2p.dal.dto.User;
import com.qdlc.p2p.dal.dto.UserCache;
import com.qdlc.p2p.dal.model.UserModel;
import com.qdlc.p2p.dal.util.UserUtils;

/**
 * 用户登录:
 * <p/>
 * 1. 检测前置条件
 * 前置条件包括:检测用户名是否存在，检测用户是否被锁定，检测请求是否需要验证码。（这是最重要的3个检测条件）
 * (a) 检测用户是否存在
 * 通过检测UserLoginExt表记录中username数据，来判断登录用户名是否存在，这里的username可以是用户名、邮箱、手机号。
 * (b) 检测用户是否锁定
 * 如果用户登录失败次数超过登录规则(login rule)中的次数，则会锁定用户登录，锁定用户登录是在Redis中保存的，锁定时间为30分钟。
 * 锁定用户登录使用UserLock对象。
 * (c) 检测请求是否需要验证码
 * 判断是否需要检测验证码，是根据登录失败次数来获取的（客户端登录失败次数）。在缓存的key值为：LOGIN_FAILURE_COUNT_KEY + 请求IP地址,
 * 以缓存的客户端的IP地址为key值标志，来判断客户端登录失败的次数，如果失败次数大于登录规则(login rule)中规定的次数，则表示需要检测验证码。
 * 2. 验证密码正确性，如果正确，调用doLoginSuccess；如果错误，调用doLoginFailure()
 * (a) doLoginSuccess
 * 登录成功后需要执行的步骤:
 * 1) 设置登录积分
 * 2) 设置UserCache中登录IP，最近登录时间等等
 * 3) 清除用户登录错误次数以及客户端登录错误次数
 * 4) 缓存userId到Redis上，作为判断用户已在线的依据（代替原来的Session），一个客户机上只能同时登录一个帐号
 * 如果多个帐号切换，会剔除原先帐号下线
 * (b) doLoginFailure
 * 登录失败后需要执行的步骤:
 * 1) 设置用户登录失败次数，以LOGIN_FAILURE_COUNT_KEY + userId作为key值，缓存时间为1天。
 * 记录用户登录失败次数，是为了锁定用户。
 * 2) 设置客户端登录失败次数，已LOGIN_FAILURE_COUNT_KEY + ip作为key值，缓存时间为1天。
 * 记录客户端登录失败次数，是为了判断是否显示验证码。
 * 3) 如果用户失败次数超过了锁定次数，则锁定当前登录用户，锁定时间为30分钟。
 * 4) 如果客户端失败次数等于需要验证码次数，则返回count=true标记，表示页面上需要显示验证码。
 * 注意：为什么需要客户端失败次数=failureCodeTime？ 因为，一旦返回conut=true后，页面上就显示了验证码，不用显示多个。
 *
 * @author linya
 */
@Service("userLoginService")
public class UserLoginServiceImpl implements UserLoginService {

    @Resource
    private UserLoginExtService userLoginExtService;

    @Resource
    private UserCacheService userCacheService;

    @Resource
    private ScoreDao scoreDao;

    //TODO 干嘛用的？
    public static final String PASSWORD_PAR = "_=3=-&efesdfp2er@#*34KD@#";

    private static final String LOGIN_FAILURE_COUNT_KEY = "user_login_failure_count_";

    /**
     * 检测用户名和登录密码
     */
    private ReturnResult checkUsernameAndPassword(String userName, String password) {
        if (StringUtil.isBlank(userName)) {
            return new ReturnResult("用户名不能为空!");
        }
        if (StringUtil.isBlank(password)) {
            return new ReturnResult("密码不能为空!");
        }
        return ReturnResult.SUCCESS;
    }

    /**
     * 检测验证码
     */
    private ReturnResult checkValidCode(String validCode) {
        String vCode = StringUtil.trimToEmpty(validCode);
        if (vCode.isEmpty()) {
            return new ReturnResult(MessageUtil.getMessage("E30005"));
        }
        if (!ValidateUtil.checkValidCode(vCode)) {
            return new ReturnResult(MessageUtil.getMessage("E30006"));
        }
        return ReturnResult.SUCCESS;
    }

    /**
     * 登录前，前置条件检测
     */
    private ReturnResult preCheckCondition(UserModel userModel) throws Exception {

        //获取操作参数
        String userName = userModel.getUserName();
        String password = userModel.getPwd();

        // 检测用户名和密码
        ReturnResult checkResult = checkUsernameAndPassword(userName, password);
        if (checkResult.isError()) {
            return checkResult;
        }

        // 检测用户是否存在，userName可以为用户名、邮箱、手机号码
        long userId = userLoginExtService.getUserIdByUserName(userModel.getUserName());
        if (userId == -1) {
            return new ReturnResult("用户名不存在!");
        }

        // 检测用户是否被锁定
        UserLock userLock = userLockStatus(userId);
        if (userLock != null) {
            return new ReturnResult(MessageFormat.format("您的帐号已被锁定,{0}分钟后解锁!", userLock.getRemainTime()));
        }

        //获取用户信息
        User user = UserUtils.getUserById(userId);
        checkResult = new ReturnResult();
        checkResult.addExtra("user", user);

        return checkResult;
    }

    /**
     * 登录操作
     *
     * @throws Exception
     */
    @Override
    public ReturnResult doLogin(UserModel userModel) throws Exception {

        // 检测登录前置条件
        ReturnResult checkResult = preCheckCondition(userModel);
        if (checkResult.isError()) {
            return checkResult;
        }

        //获取用户信息
        User user = (User) checkResult.getExtra("user");

        //检测验证码，只有超过失败次数才需要检测
        if (isShowLoginValidCode()) {
            checkResult = checkValidCode(userModel.getValidCode());
        }
        if (checkResult.isError()) {
            return checkResult;
        }

        //验证密码正确性
        String password = URLDecoder.decode(userModel.getPwd(), "utf-8");
        String passwordPar = password + PASSWORD_PAR;
        String passwordMD5 = MD5.encode(password);
        String passwordParMD5 = MD5.encode(passwordPar);
        String userPassword = user.getPwd();
        if (passwordMD5.equals(userPassword) || passwordParMD5.equals(userPassword)) {
            doLoginSuccess(user);
            return new UserReturnResult(user);
        } else {
            checkResult = doLoginFailure(user);
            return checkResult;
        }
    }

    /**
     * 用户登录成功后置处理:
     * 1. 设置登录积分
     * 2. 设置登录记录
     * 3. 清除登录错误次数缓存数据
     * 4. 缓存登录时间和用户信息
     */
    private void doLoginSuccess(User user) {
        // 登录积分
        Score item = scoreDao.findByUserId(user.getUserId());
        if (item == null) {
            item = new Score(user);
            scoreDao.save(item);
        }
        LoginScoreLog bLog = new LoginScoreLog(user.getUserId());
        bLog.doEvent();

        // 登录成功后，设置登录时间和IP
        UserCache userCache = UserUtils.getUserCacheByUserId(user.getUserId());
        userCache.setLoginTime(DateUtil.getNow());
        userCache.setLoginFailTimes(0);
        userCache.setAddIp(Global.getIP());
        userCacheService.update(userCache);

        // 清除登录次数的缓存数据
        clearLoginFailureCount(user);

        //缓存用户ID到Redis
        UserLoginUtil.cacheLoginUser(user.getUserId());
    }

    /**
     * 用户登录失败次数的key值
     *
     * @param userId
     * @return
     */
    private String wrapUserFailureCountKey(long userId) {
        return LOGIN_FAILURE_COUNT_KEY + userId;
    }

    /**
     * 客户端登录失败次数的key值
     *
     * @return
     */
    private String wrapIpFailureCountKey() {
        return LOGIN_FAILURE_COUNT_KEY + Global.getIP();
    }

    /**
     * 清除登录错误次数
     *
     * @param user
     */
    private void clearLoginFailureCount(User user) {
        String key = wrapUserFailureCountKey(user.getUserId());
        String ipKey = wrapIpFailureCountKey();
        JedisCache<Integer> jedisCache = JedisCacheFactory.newInstance(Integer.class);
        jedisCache.delete(key);
        jedisCache.delete(ipKey);
    }

    /**
     * 用户登录失败后置处理:
     * 1. 保存登录失败次数到缓存
     * 2. 如果登录失败次数超过限制，则锁定用户
     */
    private ReturnResult doLoginFailure(User user) {

        //缓存失败次数，第一次失败会设置缓存过期时间
        String failureCountKey = wrapUserFailureCountKey(user.getUserId());
        String ipKey = wrapIpFailureCountKey();
        JedisCache<Integer> jedisCache = JedisCacheFactory.newInstance(Integer.class);
        Integer failureCount = jedisCache.get(failureCountKey);
        if (failureCount == null) {
            jedisCache.set(failureCountKey, 1, 1, TimeUnit.DAYS);
            failureCount = 1;
        } else {
            jedisCache.set(failureCountKey, ++failureCount);
        }

        Integer ipFailureCount = jedisCache.get(ipKey);
        if (ipFailureCount == null) {
            jedisCache.set(ipKey, 1, 1, TimeUnit.DAYS);
            ipFailureCount = 1;
        } else {
            jedisCache.set(ipKey, ++ipFailureCount);
        }

        //获取允许的最大失败次数，如果超过最大失败次数，则锁定账户
        LoginRuleCheck loginRule = RuleCheckHelper.getValue(LoginRuleCheck.class);
        int maxFailCodeTimes = loginRule.getMax_fail_times_code();
        int maxFailTimes = loginRule.getMax_login_fail_times();

        //失败次数太多啦，账户需要暂时锁定
        if (failureCount >= maxFailTimes) {
            lockLogin(user.getUserId());
            return new ReturnResult(MessageFormat.format("您的账户已被锁定,请{0}分钟后再登录!", UserLock.USER_LOCK_MINUTES));
        }

        //如果错误次数超过需要设置验证码的次数，则在页面上显示验证码
        String errorMsg = "密码错误,已失败{0}次,失败{1}次将锁定账户!";
        ReturnResult result = new ReturnResult(MessageFormat.format(errorMsg, failureCount, maxFailTimes));
        if (ipFailureCount == maxFailCodeTimes) {
            result.addExtra("count", true);
        }
        return result;
    }

    /**
     * 判断是否需要显示登录验证码
     *
     * @return
     */
    public boolean isShowLoginValidCode() {
        LoginRuleCheck loginRule = RuleCheckHelper.getValue(LoginRuleCheck.class);
        int maxFailCodeTimes = loginRule.getMax_fail_times_code();
        String ipKey = wrapIpFailureCountKey();
        Integer failureCount = JedisCacheFactory.newInstance(Integer.class).get(ipKey);
        if (failureCount != null && failureCount > maxFailCodeTimes) {
            return true;
        }
        return false;
    }

    /**
     * 锁定登录(不是真实的锁定，在缓存中锁定)
     */
    private void lockLogin(long userId) {
        UserLock userLock = new UserLock(Global.getIP());
        JedisCacheFactory.newInstance(UserLock.class).set(userId + "", userLock, UserLock.USER_LOCK_MINUTES, TimeUnit.MINUTES);
    }

    /**
     * 判断用户是否被锁住
     *
     * @param userId
     * @return
     */
    private UserLock userLockStatus(long userId) {
        UserLock userLock = JedisCacheFactory.newInstance(UserLock.class).get(userId + "");
        return userLock;
    }

    /**
     * 锁定用户
     */
    @SuppressWarnings({"serial", "unused"})
    private static class UserLock implements Serializable {

        // 设置锁定30分钟
        static final int USER_LOCK_MINUTES = 30;

        //锁定用户的时间（毫秒）
        long lockTime;
        //锁定用户的ip
        String lockIp;

        public UserLock() {
        }

        public UserLock(String lockIp) {
            this.lockTime = System.currentTimeMillis();
            this.lockIp = lockIp;
        }

        /**
         * 获取解锁剩余时间
         */
        long getRemainTime() {
            long remain = lockTime + TimeUnit.MILLISECONDS.convert(USER_LOCK_MINUTES, TimeUnit.MINUTES) - System.currentTimeMillis();
            return TimeUnit.MINUTES.convert(remain, TimeUnit.MILLISECONDS);
        }

        public long getLockTime() {
            return lockTime;
        }

        public void setLockTime(long lockTime) {
            this.lockTime = lockTime;
        }

        public String getLockIp() {
            return lockIp;
        }

        public void setLockIp(String lockIp) {
            this.lockIp = lockIp;
        }
    }
}
